package cz.cuni.amis.pogamut.episodic.memory;

import cz.cuni.amis.pogamut.episodic.decisions.*;
import cz.cuni.amis.pogamut.episodic.episodes.ChronoLogy;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaBag;
import cz.cuni.amis.pogamut.episodic.visualizer.IVisualizationListener;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEvent;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEventType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.Semaphore;
import javax.swing.event.EventListenerList;

/**
 * AgentMemory is the main class that contains structures
 * describing the agent's memory.
 *
 * @author Zdenek Behan
 *
 * Based on the original code by Michal Cermak
 */
public class AgentMemory implements IAgentMemory, Serializable {
    /**
     * Determines if a de-serialized file is compatible with this class.
     */
    private static final long serialVersionUID = 1L;

    public Semaphore sem = new Semaphore(1, true);
    EventListenerList listeners = new EventListenerList();

    public Parameters parameters = new Parameters();

    // Local time tracker.
    private long time;

    // Visualisation related methods.
    public void addVisualizationListener(IVisualizationListener listener) {
        listeners.add(IVisualizationListener.class, listener);
    }
    public void removeVisualizationListener(IVisualizationListener listener) {
        listeners.remove(IVisualizationListener.class, listener);
    }
    public void fireVisualizationEvent(VisualizationEvent event) {
        IVisualizationListener listenerList[] = listeners.getListeners(IVisualizationListener.class);
        for (int i = 0, n = listenerList.length; i < n; i++) {
            ((IVisualizationListener) listenerList[i]).handleVisualizationEvent(event);
        }
    }

    /**
     * Instance of <code>IdGenerator</code> class providing unique IDs to
     * all nodes in agent memory. Unique IDs are needed mainly for visualizing
     * purposes.
     */
    private IdGenerator idGenerator = new IdGenerator();

    // Used to track day changes.
    private int lastDay;

    // Decision tree structure, interconnected with the actual memory nodes.
    public DecisionTree decisionTree = new DecisionTree();
    // ChronoLogy structure, containing all chronobags.
    public ChronoLogy chrono = new ChronoLogy(this, idGenerator);
    // Schema bag describes aggregated schemas, derived from past experiences.
    public SchemaBag schemas = new SchemaBag(idGenerator, this);

    /**
     * Instance of a visualizer class.
     */
//    VisualizationCreator  viz;

    // Should the visualisation be running?
    public static boolean visualizeLive = true;

    /**
     * Getter method for the <code>decisionTree</code>.
     *
     * @return  Returns the reference to <code>DecisionTree</code> structure
     * in agent's memory.
     */
    public DecisionTree getDecisionTree() {
        return decisionTree;
    }

    public IdGenerator getIdGenerator() {
        return idGenerator;
    }

    /**
     * Private method used to recursively generate unique IDs to specified
     * <code>Intention</code> node in decision tree and its subtree.
     * This is needed because the trees are received during memory
     * initialization lacking these unique IDs.
     *
     * @param i     Reference to the <code>Intention</code> that needs new ID.
     * @param idGen Reference to IdGenerator structure in agent's memory.
     */
    private void generateIntentionIds(Intention i, IdGenerator idGen) {
        i.setId(idGen.getNewId());
        for (AffordanceSlot slot : i.getAffordances()) {
            slot.setId(idGen.getNewId());
        }
        for (Action a : i.getSubNodes()) {
            generateActionIds(a, idGen);
        }
        getDecisionTree().numberOfNodes++;
     //   getDecisionTree().branchFactor.add(i.getSubNodes().size());
    }

    /**
     * Private method used to recursively generate unique IDs to specified
     * <code>Action</code> node in decision tree and its subtree.
     * This is needed because the trees are received during memory
     * initialization lacking these unique IDs.
     *
     * @param i     Reference to the <code>Action</code> that needs new ID.
     * @param idGen Reference to IdGenerator structure in agent's memory.
     */
    private void generateActionIds(Action a, IdGenerator idGen) {
        a.setId(idGen.getNewId());
        for (AffordanceSlot slot : a.getAffordances()) {
            slot.setId(idGen.getNewId());
        }
        for (AtomicAction aa : a.getAtomicActions()) {
            aa.setId(idGen.getNewId());
        }
        for (Intention i : a.getSubNodes()) {
            generateIntentionIds(i, idGen);
        }
        getDecisionTree().numberOfAtomicActions += a.getAtomicActions().size();
        getDecisionTree().numberOfNodes++;
    //    getDecisionTree().branchFactor.add(a.getAllChildrenNodes().size());
    }

    /**
     * Method used to recursively generate unique IDs to all nodes in
     * decision trees. This is needed because the trees are received
     * during memory initialization lacking these unique IDs. This method
     * should therefore be called near the end of <code>initialize</code>
     * method.
     *
     * @param i     Reference to the decision tree without set IDs.
     * @param idGen Reference to IdGenerator structure in agent's memory.
     */
    private void generateDecisionTreeIds(DecisionTree tree, IdGenerator idGen) {
        for (Intention i : tree.topLevelGoals.values()) {
            generateIntentionIds(i, idGen);
        }
    }

    @Override
    public void initialize(long epoch, int defaultSaliency) {
        // TODO: Set default saliency
        lastDay = (int) (epoch / 86400);
    }
    @Override
    public void addGoal(String name, int saliency) {
        decisionTree.topLevelGoals.put(name, new Intention(name, saliency));
    }
    @Override
    public void finalizeInit() {
        generateDecisionTreeIds(decisionTree, idGenerator);
    }
    @Override
    public boolean openNode(String node, ArrayList<String> trace, ArrayList<Affordance> affordances) {
        try {
            return addNode(node, trace, affordances);
        } catch (Exception e) {
            System.err.println("Error: "+e.getMessage());
            return false;
        }
    }
    @Override
    public boolean closeNode(String node, ArrayList<String> trace, boolean success) {
        sem.acquireUninterruptibly();
        Boolean rv;
        Collections.reverse(trace);
        try {
            rv = chrono.finishNode(node, trace, success);
        } catch (Exception e) {
            System.err.println("Error: "+e.getMessage());
            return false;
        }
        sem.release();
        return rv;
    }
    @Override
    public boolean executeAction(String atomicAction, ArrayList<String> trace, ArrayList<Affordance> affordances) {
        boolean rv;
        try {
            rv = addNode(atomicAction, trace, affordances);
            chrono.reviewActiveIntentions(trace);
        } catch (Exception e) {
            System.err.println("Error: "+e.getMessage());
            return false;
        }

        return rv;
    }

    private boolean addNode(String nodeName, ArrayList<String> trace, ArrayList<Affordance> affordances) throws Exception {
        sem.acquireUninterruptibly();

        Iterator<Affordance> itr = affordances.iterator();
        Affordance aff = null;
        while (itr.hasNext()){
            aff = itr.next();
            if (aff.type.equals("perceived") && !parameters.REMEMBER_SEEN_ITEMS) {
                itr.remove();
            }
        }

        boolean success;
        try {
            success = chrono.addNewNode(nodeName, trace, affordances, time);

        } catch (Exception e) {
            System.err.println("Error: "+e.getMessage());
            return false;
        }

        sem.release();

        //this will cause current atomic action to be displayed with current time
        if (visualizeLive) {
            String sTime = formatVisualizerTime(time);
            sTime = sTime.concat(" | ");
            sTime = sTime.concat(nodeName);

            if (chrono.present.newNodeAdded) {
                fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PRESENT_CHRONOBAG, this));
            }
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.UPDATE_TIME, sTime, this));
            chrono.present.newNodeAdded = false;
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_SCHEMA_BAG, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_CHRONOBAG_VIEW, this));
         }
        return success;
    }

    @Override
    public void advanceTime(long epoch) {
        time += epoch;
        return;
    }
    @Override
    public void advanceDay(long days) { advanceTime(days*86400); }
    @Override
    public void advanceHour(long hours) { advanceTime(hours*3600); }
    @Override
    public void advanceMinute(long minutes) { advanceTime(minutes*60); }
    @Override
    public void updateTime(long epoch) {
        time = epoch;
        return;
    }

    public void initVisualizers() {
        if (visualizeLive) {
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.INITIALIZATION, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PAST_CHRONOBAGS, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PRESENT_CHRONOBAG, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_CHRONOBAG_VIEW, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_SCHEMA_BAG, this));
        }
    }

    private String formatVisualizerTime(long time) {
        // This format is expected by the memory model.
        int min = (int) ((time / 60) % 60);
        int hour = (int) ((time / 3600) % 24);
        int day = (int) (time / 86400);
        String dayOfWeek = new String();
        switch(day % 7) {
            case 0: dayOfWeek = new String("Mo"); break;
            case 1: dayOfWeek = new String("Tu"); break;
            case 2: dayOfWeek = new String("We"); break;
            case 3: dayOfWeek = new String("Th"); break;
            case 4: dayOfWeek = new String("Fr"); break;
            case 5: dayOfWeek = new String("Sa"); break;
            case 6: dayOfWeek = new String("Su"); break;
        }

        return String.format("%s, %02d:%02d, day %d.", dayOfWeek, hour, min, day );
    }

    @Override
    public void performSleep() {
        chrono.performSleep();
        schemas.performSleep();

        if (visualizeLive) {
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PAST_CHRONOBAGS, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PRESENT_CHRONOBAG, this));
            fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_CHRONOBAG_VIEW, this));
        }
    }

    /**
     * Generates a csv file with information how different goals were satisfied.
     * Generated data is based on the actual events from the simulation.
     * All actions performed to satisfy certain goal are assigned percentages
     * providing distribution how often the goal was satisfied with each action.
     */
    public void generateStatistics(String fileName) {
        schemas.generateStatistics(fileName);
    }
}
